from IPython.display import Image
Profesores:
Integrantes:
En el mundo actual las redes sociales se han posicionado como un elemento masivo de intercambio de información. En él, se han hecho partícipes todos los elementos de la sociedad, desde personas comunes hasta celebridades, empresas, políticos, etc.
Históricamente Twitter ha sido una de las redes sociales por excelencia, actualmente no ha dejado de serlo, es más, al 25 de enero cuenta con mas de 320 millones de usuarios activos en el último mes.
A fines del 2018, la Comisión de Seguridad y Energía (SEC) llevó a Elon Musk a juicio. La razón, sus tweets que causaban especulaciones en la Bolsa de Acciones, desde mensajes aludiendo la posible recompra de las acciones de Tesla, sin realmente hacerlo, a especulaciones sobre la demanda. Se estima que Elon Musk ha generado significativas inestabilidades en la bolsa.
Image(filename='musk_1.png')
Por lo anterior, la SEC argumenta que Elon Musk no debería tener la libertad de hacer tweets sin regulación por parte del equipo legal de la empresa, pero dado que decisiones de ese estilo implican intervenir la libertad de expresión de un hombre, se hace imperativo estudiar si la SEC tiene una base significativa para sus argumentos.
Así, se ha decidido realizar minería de datos, para conocer la influencia que tienen los tweets de una figura tan reconocida como Elon Musk sobre el precio de las acciones de su compañía.
En este proyecto se espera poder responder las siguientes preguntas:
A continuación se muestra el código que se hizo para realizar el procesamiento de los datos, junto con acotaciones y análisis que se desprenden de los resultados.
%matplotlib inline
import numpy as np
import pandas as pd
import datetime
import seaborn as sns
import matplotlib.pyplot as plt
import gensim
import spacy
import pyLDAvis.gensim
import os, re, operator, warnings
import spacy
from matplotlib import dates
from gensim.models import CoherenceModel, LdaModel, LsiModel, HdpModel
from gensim.models.wrappers import LdaMallet
from gensim.corpora import Dictionary
from wordcloud import WordCloud, STOPWORDS, ImageColorGenerator
warnings.filterwarnings('ignore')
nlp = spacy.load('en_core_web_sm')
pd.set_option('display.max_colwidth', -1)
sns.set()
sns.set_context('notebook',font_scale=2.2)
sns.set_style('ticks')
plt.rcParams['figure.figsize'] = (15,8)
Se obtuvieron 3 datasets con los tweets de Elon Musk en diferentes períodos de tiempo, por lo que se partirá el con el proceso requerido para juntarlos. Cabe destacar que estos datasets se obtuvieron de la página web Kaggle.
ten = pd.read_csv("elonmusk_tweets.csv", delimiter=',', header=0)
print('Muestra del primer dataset:')
display(ten.head())
twelve = pd.read_csv("data_elonmusk.csv", delimiter=',', header=0, encoding="unicode_escape")
print('Muestra del segundo dataset:')
display(twelve.head())
seventeen = pd.read_json("user-tweets.jsonl", lines=True)
print('Muestra del tercer dataset:')
display(seventeen.head())
Se observa que en cada dataset se tienen atributos diferentes, por lo tanto se deben guardar los atributos que están presentes en los 3 datasets, esos son: fecha y tweet.
ten = pd.DataFrame({"date": ten["created_at"], "tweet": ten["text"]})
twelve = pd.DataFrame({"date": twelve["Time"], "tweet": twelve["Tweet"]})
seventeen = pd.DataFrame({"date": seventeen["CreatedAt"], "tweet": seventeen["Text"]})
Como se desean juntar todos los datasets, se estandariza la notación para la fecha, eso implica eliminar los segundos del dataset ten y twelve y reformatear seventeen en formato AAAA-MM-DD HH:MM, dado que este formato entrega resultados consistentes de comparación sobre sus strings.
ten["date"] = ten["date"].str[:-3]
twelve["date"] = twelve["date"].str[:-3]
def formatDate(date):
months = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"]
date_list = date.split(" ")
month = "%02d" % (months.index(date_list[0]) + 1)
day = date_list[1][:-1] #delete ","
year = date_list[2]
if date_list[4][-2:] == "AM":
time = date_list[4][:-2]
else:
aux = int(date_list[4][:2]) % 12 + 12
time = ("%02d" % aux) + date_list[4][2:-2]
return year + "-" + month + "-" + day + " " + time
seventeen["date"] = seventeen["date"].apply(lambda x: formatDate(x))
Otra modificación necesaria será eliminar un "b" que se encuentra al principio de todos los tweets del dataset ten
ten["tweet"] = ten["tweet"].str[1:]
ten["tweet"] = ten["tweet"].str[1:-1]
Ahora se deben buscar que rangos de tiempo cubre cada dataset para así juntarlos
print("Dataset ten va desde {} a {}".format(min(ten["date"]), max(ten["date"])))
print("Dataset twelve va desde {} a {}".format(min(twelve["date"]), max(twelve["date"])))
print("Dataset seventeen va desde {} a {}".format(min(seventeen["date"]), max(seventeen["date"])))
Con lo anterior, el único cambio a hacerse es considerar sólo los tweets de twelve que se hayan hecho posterior a 2017-04-05 14:56.
twelve = twelve[twelve["date"] > "2017-04-05 14:56"]
Ahora se puede hacer un sort por fecha de cada dataset (ascendiente) y concatenarlos.
ten = ten.sort_values("date", ascending=True)
twelve = twelve.sort_values("date", ascending=True)
seventeen = seventeen.sort_values("date", ascending=True)
em_tweets = pd.concat([ten, twelve, seventeen])
em_tweets.describe()
em_tweets.head()
Se revisa si hay entradas vacías.
empty_mask = em_tweets[(em_tweets["tweet"] == "") | (em_tweets["date"] == "")]
empty_mask
Se eliminan las entradas vacías
em_tweets.drop(list(empty_mask.index.values), inplace=True)
También se ve que hay tweets repetidos (en el describe del dataframe), así se debe ver si son tweets que se captaron dos veces (tiempos cercanos) o si efectivamente se repitieron por Elon Musk (en cuyo caso no se eliminan). Para esto primero se eliminarán los tweets, que tienen tanto el mismo mensaje como fecha y hora de publicación.
em_tweets.drop(list(em_tweets[em_tweets.duplicated()].index.values), inplace=True)
Ahora se verá manualmente que tweets repetidos hay, para ver si efectivamente se hicieron más de una vez o se captaron más de una vez.
repeated = em_tweets[em_tweets["tweet"].duplicated(keep=False)]
repeated
Viendo las fechas, y revisando twitter manualmente, el único tweet que se consideró más de una vez es el de índice 0, por lo que se eliminará
em_tweets.drop([0], inplace=True)
years = pd.DataFrame({"year": em_tweets[em_tweets["date"] >= "2011-01-01 00:00"]["date"].apply(lambda x: x.split(" ")[0].split("-")[0]).astype("int")})
Dado que hay sólo 1 tweet del 2010, este se ignorará para el histograma
a = pd.DataFrame({"freq": years["year"].value_counts()})
a["year"] = a.index
a.sort_values(["year"], inplace=True)
plt.figure()
plt.gca().invert_yaxis()
plt.barh(a["year"], a["freq"])
plt.grid()
plt.xlabel("Número de Tweets")
plt.ylabel("Año")
plt.yticks(list(a["year"]))
plt.title("Número de Tweets de Elon Musk por año");
Como se ve, la actividad de Elon Musk en Twitter ha aumentado considerablemente con el pasar de los años, aún así no es de fiarse completamente este gráfico, dado que es posible que existan tweets que no se registraron en los datasets. Ahora se estudiarán las horas con más actividad en Twitter por parte de Elon Musk.
timings = pd.DataFrame({"time": em_tweets["date"].apply(lambda x: x.split(" ")[1].split(":")[0] + x.split(" ")[1].split(":")[1])
.astype("int")})
hours = np.arange(0, 2401, 100).astype(str)
for ind in range(len(hours)):
hour = hours[ind]
if len(hour) == 3:
hours[ind] = "0"+hour[0]+":"+hour[1:]
elif len(hour) == 4:
hours[ind] = hour[0:2]+":"+hour[2:]
else:
hours[ind] = "0"+hour+":"+"00"
timings.hist(bins=range(0, 2401, 100), alpha=0.8, histtype='bar', ec='black', xrot=45)
plt.xlabel("Hora de publicación (Hora Pacífico)")
plt.ylabel("Frecuencia")
plt.xticks(range(0, 2401, 100),hours)
plt.title("Hora de publicación de los tweets de Elon Musk");
Se puede observar que Elon Musk suele twittear todo el día, con peaks durante la tarde y la madrugada, aunque vemos un período en el mediodía en el que también se registró una cantidad significativa de tweets. Una posible hipótesis que explique las horas en que se hacen las publicaciones, son los contantes viajes que realiza Musk, haciendo que se encuentre en distintos husos horarios.
A continuación, se realiza el procedimiento para obtener las palabras más comunes en los tweets de Elon (excluyendo palabras que no aportan información).
tweetss = em_tweets.loc[:,'tweet']
conc = ""
for sstring in tweetss:
conc = conc + sstring
Se agregarán un par de frases que serían un buen marcador de texto que se debe ignorar, de todas formas ya están consideradas las palabras que comúnmente no aportan significado a una oración.
my_stop_words = [u' ',u' ', u'http',u'amp']
for stopword in my_stop_words:
lexeme = nlp.vocab[stopword]
lexeme.is_stop = True
doc = nlp(conc)
# we add some words to the stop word list
counter = 0
texts, article = [], []
for w in doc:
# if it's not a stop word or punctuation mark, add it to our article!
if w.text != '\n' and not w.is_stop and not w.is_punct and not w.like_num:
# we add the lematized version of the word
article.append(w.lemma_)
# if it's a new line, it means we're onto our next document
if w.text == '\n':
counter += 1
article = [article]
bigram = gensim.models.Phrases(article)
texts = [bigram[line] for line in article]
trigram = gensim.models.Phrases(texts)
texts1 = [trigram[line] for line in texts]
texts[0][:10]
dictionary = Dictionary(texts)
corpus = [dictionary.doc2bow(text) for text in texts]
elem_dict, index_dict, index_corpus, frequency_corpus = [], [], [], []
for k, v in dictionary.token2id.items():
elem_dict.append(k)
index_dict.append(v)
for i in corpus[0]:
index_corpus.append(i[0])
frequency_corpus.append(i[1])
elem_1 = np.array(elem_dict)
index_1 = np.array(index_dict)
index_2 = np.array(index_corpus)
frequency_1 = np.array(frequency_corpus)
dict_df = pd.DataFrame({"indice" : index_1,"elemento" : elem_1})
corpus_df = pd.DataFrame({"indice" : index_2,"frecuencia" : frequency_1})
word_frequency = pd.merge(dict_df,corpus_df)
word_frequency = word_frequency.sort_values(by="frecuencia",ascending=False)
word_frequency.iloc[0:10]
freq_dict = {}
word_freq = word_frequency.values
for i in range(word_freq.shape[0]):
freq_dict[word_freq[i, 1]] = word_freq[i, 2]
wordc = WordCloud(width=1920,
height=1080,
background_color="white",
max_words=100,
random_state = 97).generate_from_frequencies(freq_dict)
plt.figure()
plt.imshow(wordc, interpolation='bilinear')
plt.axis("off")
plt.savefig('WordMap.png',dpi=300)
Al observar la imagen a anterior, se nota la alta presencia de palabras que hacen referencia a la empresa Tesla, Inc. ya sea de manera directa o indirecta. Por ejemplo, dos de las palabras más utilizadas son Tesla y @Tesla. Además, palabras como car y model, muestran una predominancia en un segundo plano. Todos estos términos pueden relacionarse con Tesla, Inc.
En conjunto con esto, se observan palabras que hacen referencia a otras empresas de las que Elon Musk forma parte, en específico, el termino, landing, launch y SpaceX hacen mención a la empresa de transporte aeoespacial SpaceX.
word_frequency.iloc[0:10,:].plot.bar(x='elemento', y='frecuencia')
plt.grid()
plt.title("10 palabras (significativas) más comúnes en los tweets de Elon Musk")
plt.xlabel("Palabra")
plt.ylabel("Frecuencia")
A continuación se agrega un dataset que muestra el precio de la acción de Tesla, Inc por día, indicando el precio de entrada, de salida, el máximo, el mínimo, el día y el volumen de dinero transado en un día. Se detalla que este dataset se obtuvo desde el sitio web Yahoo.
price = pd.read_csv("TSLA.csv", delimiter=',', header=0)
print(price.head())
En el gráfico a continuación, se muestra la evolución del precio de la acción de Tesla, Inc, donde además se aplico un filtro suavizador de tipo moving average para tener una noción de la tendencia que siguen los datos, dado que naturalemente las acciones son un mercado con altas fluctuaciones día a día, por lo que al graficar los datos sin preprocesar se tiene una muy alta frecuencia que no permite evidenciar de manera muy clara la tendencia.
converted_dates = list(map(datetime.datetime.strptime, price["Date"], price["Date"].values.shape[0]*['%Y-%m-%d']))
x_axis = converted_dates
formatter = dates.DateFormatter('%Y-%m-%d')
y_list_names = list(price.columns[1:-2])
plt.figure()
for name in y_list_names:
window = 15
y = price[name]
avg_mask = np.ones(window)/window
y_conv = np.convolve(y,avg_mask,'valid')
plt.plot(x_axis[:-14], y_conv, '-', label=name)
plt.title("Precio de las acciones de Tesla Inc. (TSLA) en el tiempo")
plt.xlabel("Fecha")
plt.ylabel("Precio por acción ($USD)")
plt.legend();
Dado que se observa que los distintos identificadores del precio de la acción muestran un comportamiento similar, se elige uno en particular para seguir trabajando, en específico, se toma como referencia al valor Open.
y_eje=price["Open"]
window = 15
avg_mask = np.ones(window)/window
y_conv = np.convolve(y_eje,avg_mask,'same')
plt.figure(figsize=(15,8))
plt.plot(x_axis[:-14],y_conv[:-14],linewidth=5)
plt.title("Precio de las acciones de Tesla Inc. en el tiempo (Open)")
plt.xlabel("Año")
plt.ylabel("Precio por acción ($USD)")
plt.savefig('accion.png',dpi=300);
En el gráfico anterior se muestra el precio de apertura (Open) diario de la acción de Tesla, Inc., abarcando el período de junio de 2010 hasta la actualidad. Se observa que en el período previo a 2013, la fluctuación del precio estaba acotada y mostraba un comportamiento casi constante respecto al tiempo.
Pese a esto, para los siguientes años se pueden notar claramente dos periodos de tiempo en que las acciones tienen alzas sostenidas, desde 2013 en adelante el precio muestra un incremento considerable, alcanzando aproximadamente cinco veces su valor en un año, donde se estabiliza por los siguientes tres años.
Luego, a finales de 2017 vuelve a mostrar un incremento en el precio. Justamente estos periodos correspondes a momentos posteriores a lanzamientos de nuevos vehículos, ya sea el Tesla Model S, que fue lanzado a mediados del 2012 y Tesla Model 3 que fue presentado a principios del 2016, con las primeras entregas a mediados del 2017.
Para realizar experimentos con los datos, se decidió por tratar en primera instancia los tweets y el precio de la acción por separado. Dado esto a continuación se explica el tratamiento de los tweets.
Se entrena un modelo utilizando word2vec, que realiza una distribución vectorial de las palabras de los tweets considerando la distantancia de hamming, es decir, mientras más similares son las palabras estarán más cercanas. Este modelo comienza ubicando la primera palabra recibida en la posición (0,0) y distribiyendo las siguientes según la similitud que tienen.
En una primera instancia se entrena el modelo para que represente las palabras en 2 dimensiones, pasando luego a una representación en 100 dimensiones. Dado que hay procesos que se deben realizar varias veces, se crean funciones que permitan generalizar los procedimientos de representación y visualización, evitando así repetir de manera innecesaria código.
Por lo anterior, se crea la función model_word2vec la cual a través de un conjunto de tweets entrena un modelo de representación vectorial de palabras. Luego, el modelo se encarga de determinar la representación vectorial de cada tweet y aplicar reducción de dimensionalidad de ser necesario.
Además, se crea la función clusters_plot la cual toma la representación vectorial de un conjunto de tweets y se encarga de aplicar técnicas de clustering sobre los datos, ya sea K-Means o DBSCAN, por último se grafica el resultado del clustering.
Es importante notar que el modelo de representación vectorial asigna a cada palabra un vector en n dimensiones, luego, para poder representar un tweet se decidió por representarlo a través del vector promedio de cada palabra que conforma el tweet.
from gensim.models import Word2Vec
from sklearn.cluster import KMeans, DBSCAN
from sklearn.manifold import TSNE
def model_word2vec(data, min_count_w = 1, size_rep = 2, window_w = 5, random_seed = 68,
TSNE_r = False, ndim_f = None, TSNE_seed = 42):
new_doc = np.array(data)
# Cleaning data - remove punctuation from every newsgroup text
sentences = []
# Go through each text in turn
for ii in range(len(new_doc)):
sentences = [re.sub(pattern=r'[\!"#$%&\*+,-./:;<=>?@^_`()|~=]',
repl='',
string=x
).strip().split(' ') for x in new_doc[ii].split('\n')
if not x.endswith('writes:')]
sentences = [x for x in sentences if x != ['']]
new_doc[ii] = sentences
all_sentences = []
for text in new_doc:
all_sentences += text
model = gensim.models.Word2Vec(all_sentences, min_count = min_count_w,
size = size_rep, window = window_w, seed = random_seed)
def mean_vector(sentence , model):
new_sen = sentence.split(" ")
n = 0
result = np.zeros(size_rep,dtype=float)
for word in new_sen:
if word in model.wv.vocab:
result += model.wv[word]
n+=1
if n==0: return result
return result/n
tweets = np.array([data.apply(lambda x:mean_vector(x,model))])
tweets = tweets[0,:,:]
if TSNE_r:
tweets = TSNE(n_components=ndim_f,random_state=TSNE_seed).fit_transform(tweets)
return tweets
def clusters_plot(data, n_class = 2, method = 'KMeans', rand_st = np.random.randint(1), x_label='Eje X', y_label='Eje Y',
title = None, leg_loc = 0, leg_size = 2, ms = 3, print_samples = False, org_data = None, n_samples = 5,
eps_c=5.5, min_samples_c=2):
if method == 'KMeans':
metodo = KMeans(n_clusters=n_class,random_state=rand_st).fit(data)
centroids = metodo.cluster_centers_
elif method == 'DBSCAN':
metodo = DBSCAN(eps=eps_c, min_samples=min_samples_c).fit(data)
labels = metodo.labels_
if not title:
title = f'{len(set(labels))} Clusters de Tweets'
plt.figure()
for i in set(labels):
plt.scatter(data[labels==i,0],
data[labels==i,1],
label = f'Cluster {i}',
s = ms)
if method == 'KMeans':
plt.plot(centroids[:,0],centroids[:,1],'ko',markersize=8, label='centroides')
plt.legend(loc=leg_loc,markerscale=leg_size);
plt.xlabel('eje X')
plt.ylabel('eje Y')
plt.title(title)
if print_samples:
if org_data is None:
print('Datos originales faltantes')
else:
for i in range(len(set(labels))):
print(f'Muestras de la etiqueta {i}:'), print('')
print(org_data['tweet'].iloc[labels == i][:n_samples]), print('')
return metodo
tweets_0001 = model_word2vec(em_tweets['tweet'])
En un primer intento de representación se opto por 2 dimensiones, dado que un modelo de esta forma otorga la posibilidad de visualizar los datos y así explorar separaciones naturales en los tweets. A continuación se presenta el gráfico obtenido con esta representación.
plt.scatter(tweets_0001[:,0], tweets_0001[:,1], s = 3)
plt.xlabel('eje X')
plt.ylabel('eje Y')
plt.title('Representacion de Tweets en el espacio');
Se observa que la representación en dos dimensiones no logra captar una diferencia notable entre los datos, teniendo una rperesentación que se ajusta a una recta. A pesar de lo anterior, se realiza clustering utilizando K-means, en esta primera instancia se grafica para 2 cluster dejando en evindencia los centroides utilizados. También se entrega una muestra de los elementos que se encuentran en cada uno de los cluster.
clusters_plot(tweets_0001, n_class = 2, rand_st = 10, print_samples = True, org_data = em_tweets);
Se observa los clusters no contienen información relevante que apriori se pueda utilizar en el proyecto, esto dado que la representación en dos dimensiones pierde la capacidad de mostrar agrupaciones naturales de los datos.
Antes de pasar a modelos de mayor dimensión, se realiza el mismo procedimiento, pero buscando 3 cluster. Nuevamente se grafica y se representa la ubicación de los centroides, además de entregar una muestra de los elementos pertenecientes a cada cluster
clusters_plot(tweets_0001, n_class = 3, rand_st = 10, print_samples = True, org_data = em_tweets);
Nuevamente se observa que los clusters no contienen información representativa que permita establecer una diferencia entre ellos, esto implica que se deben utilizar modelos de representación en espacios de más dimensiones, dado que es posible que esto permita obtener más características de los datos.
Se entrena otro modelo vectorial con las mismas palabras de los Tweets, pero esta vez la dimensionalidad se aumenta a 100. Aumentar la dimensionalidad entrega una mayor diferenciación entre las palabras. Luego se realiza el cálculo para obtener la posicion de los tweets y finalmente se aplica una reducción de dimensionalidad para tener una representación visual de 2 dimensiones de los datos.
tweets_0002 = model_word2vec(em_tweets['tweet'], size_rep = 100, random_seed = 47,
TSNE_r = True, ndim_f = 2, TSNE_seed = 42)
plt.plot(tweets_0002[:,0],tweets_0002[:,1],'b.',alpha=.5)
plt.xlabel('eje X')
plt.ylabel('eje Y')
plt.title('Reducción de 100 a 2 dimensiones');
Se observa que esta representacion muestra agrupaciones de datos, sobre los cuales se pueden explorar sus características para luego obtener información útil para el proyecto. Se utilizará tanto K-means como DBSCAN.
A la representación vectorial reducida de 100 a 2 dimensiones se le aplica K-means para encontrar 2 cluster, los que se representan en el gráfico, en conjunto a sus centroides. Prosteriormente se entrega una muetra de los datos obtenidos en cada cluster.
clusters_plot(tweets_0002, n_class = 2, rand_st = 10, print_samples = True, org_data = em_tweets);
Se repite el procedimiento para la obtención de 3 cluster, entregando los resultados correspondientes.
clusters_plot(tweets_0002, n_class = 3, rand_st = 10, print_samples = True, org_data = em_tweets);
Se observa que K-Means no otorga buenos resultados ni con dos ni con tres clusters, y es probable que este resultado se mantenga al aumentar el número de clusters. Esto se tiene dado que los datos se distribuyen de forma dispersa en el plano, luego, se hace más relevante detectar zonas de densidad de puntos. El algoritmo que ayuda en datos con características como las anteriores es DBSCAN.
Cambiando el método para obtener los clusters a DBScan, se fija un eps=4, para obtener una cantidad de cluster que sean bien definidos dentro de las 2 dimensiones, luego se grafica cada uno de los cluster obtenidos y se entrega una muestra de los tweet presentes en cada uno de ellos.
dbscan_01 = clusters_plot(tweets_0002, method = 'DBSCAN', eps_c = 4, rand_st = 10, print_samples = True, org_data = em_tweets);
Aca se puede observar con mayor claridad clusters que tienen sentido semantico, por ejemplo hay clusters que contienen respuestas cortas como "no", "yes", "exactly", además hay clusters donde se agrupan hashtags. Dado esto, una idea a seguir es eliminar los tweets que a priori parecen no aportar con mayor información para el objetivo del proyecto y así poder realizar clustering nuevamente con los datos más limpios. Cabe destacar que para que la limpieza de datos sea más representativa aún, se debe reentrenar el modelo de representación vectorial, pero quitando los tweets que en esta instancia de detectaron como ruido.
labels_01 = dbscan_01.labels_
non_info_tags_01 = [-1,11,12,14,15]
labels_tags_01 = np.zeros(em_tweets.shape[0])
for i in non_info_tags_01:
labels_tags_01 = labels_tags_01 + (labels_01==i)
cleaned_tweets_01 = pd.DataFrame(data=em_tweets.values[labels_tags_01==0],columns=['date','tweet'])
print(f'Cantidad de tweets a ser eliminados: {labels_tags_01.sum()}')
print(f'Cantidad de tweets restantes: {cleaned_tweets_01.shape[0]}')
tweets_0003 = model_word2vec(cleaned_tweets_01['tweet'], size_rep = 100, random_seed = 47,
TSNE_r = True, ndim_f = 2, TSNE_seed = 42)
dbscan_02 = clusters_plot(tweets_0003, eps_c=4, method = 'DBSCAN', rand_st = 10, print_samples = True, org_data = cleaned_tweets_01);
Se observan clusters más separados y además aumento la cantidad de puntos en cada uno, luego, esto implica que los datos están más limpios que antes y por tanto su división en el espacio es más evidente. Aún así, se pueden observar clusters que contienen información no relevante para el proyecto, luego, se repite el proceso de limpieza quitando estos clusters y reentrenando el modelo.
labels_02 = dbscan_02.labels_
non_info_tags_02 = [-1,1,3]
labels_tags_02 = np.zeros(cleaned_tweets_01.shape[0])
for i in non_info_tags_02:
labels_tags_02 = labels_tags_02 + (labels_02==i)
cleaned_tweets_02 = pd.DataFrame(data=cleaned_tweets_01.values[labels_tags_02==0],columns=['date','tweet'])
print(f'Cantidad de tweets a ser eliminados: {labels_tags_02.sum()}')
print(f'Cantidad de tweets restantes: {cleaned_tweets_02.shape[0]}')
tweets_0004 = model_word2vec(cleaned_tweets_02['tweet'], size_rep = 100, random_seed = 47,
TSNE_r = True, ndim_f = 2, TSNE_seed = 42)
dbscan_03 = clusters_plot(tweets_0004, method = 'DBSCAN', eps_c = 4,rand_st = 10, print_samples = True, org_data = cleaned_tweets_02, n_samples = 7);
En el último gráfico se observan clusters con mayor concentración de datos y además se observan menos clusters de datos con información no relevante, luego se realiza por última vez el proceso de limpiar los datos quitando los tweets que no aportan para el proyecto.
print(cleaned_tweets_02.shape[0])
print(dbscan_03.labels_.shape)
tweets_0004.shape
labels_03 = dbscan_03.labels_
non_info_tags_03 = [-1,0]
labels_tags_03 = np.zeros(cleaned_tweets_02.shape[0])
for i in non_info_tags_03:
labels_tags_03 = labels_tags_03 + (labels_03==i)
cleaned_tweets_03 = pd.DataFrame(data=cleaned_tweets_02.values[labels_tags_03==0],columns=['date','tweet'])
print(f'Cantidad de tweets a ser eliminados: {labels_tags_03.sum()}')
print(f'Cantidad de tweets restantes: {cleaned_tweets_03.shape[0]}')
tweets_0005 = model_word2vec(cleaned_tweets_03['tweet'], size_rep = 100, random_seed = 47,
TSNE_r = True, ndim_f = 2, TSNE_seed = 42)
dbscan_04 = clusters_plot(tweets_0005, method = 'DBSCAN', rand_st = 10, print_samples = True, org_data = cleaned_tweets_03);
Así, es que el último modelo reduce en gran medida la cantidad de clusters, pero al mismo tiempo se observa que disminuyó la cantidad de puntos, esto dado la sucesiva limpieza. Luego, se puede concluir que incluso borrando los datos que son ruido y no aportan a demostrar o rechazar la hipótesis, no se logra encontrar separación evidente entre los clusters. Esto indica que en realidad los tweets de Elon Musk no se clasifican en buenos o malos, o alguna categorización binaria, es más, se observa que muy pocos tweets hablan de su empresa en forma tal que se pueda ver afectado el precio de la acción, luego, los experimentos de clustering permiten concluir que a priori no existe uan relación entre los tweets y el precio de la acción dado que no existen agrupaciones naturales en los tweets. Se buscará confirmar esta afirmación con experimentos de clasificación y regresión.
Para los tweet del modelo vectorial de 100 dimensiones K-means para distintos valores de k con la finalidad de obtener el optimo mediante el metodo del codo.
from scipy.spatial.distance import cdist
tweets_100 = model_word2vec(em_tweets['tweet'], size_rep = 100, random_seed = 47)
distortions = []
K = range(1,10)
for k in K:
kmeanModel = KMeans(n_clusters=k,random_state=97).fit(tweets_100)
kmeanModel.fit(tweets_100)
distortions.append(sum(np.min(cdist(tweets_100, kmeanModel.cluster_centers_, 'euclidean'), axis=1)) / tweets_100.shape[0])
# Se grafica el codo
plt.plot(K, distortions, 'bx-', linewidth= 5)
plt.xlabel('k cantidad de clusters')
plt.ylabel('Distorción')
plt.title('Método del codo');
Observando el gráfico se puede considerar que el número óptimo de clusters es 3.
Antes de descartar por completo clustering, se prueba una última vez con una representación en 100 dimensiones sin reducción de dimensionalidad. así, utilizando k=3 se realiza K-means, para obtener los cluster representativos de los tweets, de cada uno de los 3 cluster se encuentra una muetra de algunos de sus tweets.
kmeanModel_opt = KMeans(n_clusters=3,random_state=97).fit(tweets_100)
etiquetas_100 = kmeanModel_opt.labels_
for i in range(len(set(etiquetas_100))):
print(f'Muestras de la etiqueta {i+1}:'), print('')
print(em_tweets['tweet'].iloc[etiquetas_100 == i][-5:]), print('')
Nuevamente se observa que no es posible obtener características relevantes para el proyecto, por lo que se procede con los otros métodos, como clasificación y regresión.
En esta sección se obtendrán características a partir de los datos, para así aplicar técnicas de Aprendizaje Supervisado como Clasificación y Clustering. Vamos a partir obteniendo el subconjunto de tweets relacionados a Tesla, del dataset original.
tesla_tweets = em_tweets[em_tweets["tweet"].apply(lambda x: "Tesla" in x or "Model" in x or "car" in x)]
tesla_tweets.head()
Filtramos por tweets después del 2012, dado que desde esa fecha hay un flujo constante de tweets.
tesla_tweets = tesla_tweets[tesla_tweets["date"] > "2012-01-01 00:00"]
tesla_stock = price[price["Date"] > "2012-01-01"]
Dado el ruido que existe en el precio de la ación, se estudiará que tamaño de ventana puede ser útil para representar la evolución del precio de la acción, dado que se quiere reducir ruido de la señal, pero no perder la inmediatez que podría tener el efecto de un tweet.
filters = [1, 3, 5, 10, 20]
smoothened_stock = []
for i in filters:
smoothened_stock.append(np.convolve(tesla_stock["Open"], np.ones((i,))/i, mode='valid'))
for i in range(1, len(smoothened_stock)):
converted_dates = list(map(datetime.datetime.strptime, tesla_stock["Date"], tesla_stock["Date"].values.shape[0]*['%Y-%m-%d']))
x_axis = converted_dates
formatter = dates.DateFormatter('%Y-%m-%d')
plt.figure(figsize=(15, 10))
y_axis = tesla_stock["Close"]
plt.plot(x_axis, y_axis, '-', c='b', label="Señal original")
aux = str(filters[i])
plt.plot(x_axis[filters[i] - 1:], smoothened_stock[i], c='r', label="MA sobre la señal, ventana="+aux)
plt.title("Precio de las acciones de Tesla Inc. (TSLA) en el tiempo", fontsize=24)
plt.xlabel("Fecha", fontsize=16)
plt.ylabel("Precio por acción ($USD)", fontsize=16)
ax = plt.gcf().axes[0]
ax.xaxis.set_major_formatter(formatter)
plt.gcf().autofmt_xdate(rotation=25)
plt.legend(prop={"size": 16})
plt.show()
Ahora se obtiene el número de Tweets publicados en ventanas de tiempo anteriores a un dato del dataframe de la acción.
Dados los gráficos obtenidos, se usará la media móvil con tamaño de ventana = 5, dado que se reduce el ruido, pero siguen existiendo saltos no suaves, que podrían codificar la inmediatez del cambio que pudo haber sido inducida por algún tweet.
def simple_features(stock, tweets, days_window, count=False):
dates = stock["Date"].values.astype("str")
tweet_dates = tweets["date"]
num_stock = dates.shape[0]
features = np.zeros((num_stock - days_window))
for i in range(days_window, num_stock):
date = dates[i] + " 00:00"
prev_date = dates[i - 1]
n_prev_date = datetime.datetime.strptime(prev_date, "%Y-%m-%d") - datetime.timedelta(days_window - 1)
aux_month = str(n_prev_date.month).zfill(2)
aux_day = str(n_prev_date.day).zfill(2)
n_prev_date_str = f"{n_prev_date.year}-{aux_month}-{aux_day} 00:00"
num_tweets = tweet_dates[tweet_dates.apply(lambda x: x > n_prev_date_str and x < date)]
if count:
features[i - days_window] = int(num_tweets.values.shape[0])
else:
features[i - days_window] = int(num_tweets.values.shape[0] > 0)
return features
features_1 = simple_features(tesla_stock, tesla_tweets, 1)
features_3 = simple_features(tesla_stock, tesla_tweets, 3)
features_1_2 = simple_features(tesla_stock, tesla_tweets, 1, True)
features_3_2 = simple_features(tesla_stock, tesla_tweets, 3, True)
A partir de la media móvil de ventana de tamaño 5, se crea la característica que codifica si la media móvil aumentó de un período a otro.
growth = np.reshape((np.diff(smoothened_stock[2]) > 0).astype(int), (-1, 1))
Se cargan los tweets de Tesla que tienen además el label del sentimiento (positivo, negativo o neutro), asociado a cada tweet, esta se obtuvo etiquetando los tweets a mano.
sentiments = pd.read_csv("sentiment_labels_clean.csv", delimiter=",", index_col=0)
A partir del dataset anterior, se define la característica de sentiment score, que vendría a ser la suma del número de tweets con sentimiento positivo menos el número de tweets con sentimiento negativo, en una ventana de tiempo anterior a un dato de la acción.
def sentiment_score(stock, tweets_labelled, days_window):
dates = stock["Date"].values.astype("str")
tweet_dates = tweets_labelled["date"]
tweet_score = tweets_labelled["label"]
num_stock = dates.shape[0]
features = np.zeros((num_stock - days_window))
for i in range(days_window, num_stock):
date = dates[i] + " 00:00"
prev_date = dates[i - 1]
n_prev_date = datetime.datetime.strptime(prev_date, "%Y-%m-%d") - datetime.timedelta(days_window - 1)
aux_month = str(n_prev_date.month).zfill(2)
aux_day = str(n_prev_date.day).zfill(2)
n_prev_date_str = f"{n_prev_date.year}-{aux_month}-{aux_day} 00:00"
num_tweets = tweet_score[tweet_dates.apply(lambda x: x > n_prev_date_str and x < date)]
features[i - days_window] = int(np.sum(num_tweets.values))
return features
Finalmente, se crean 5 combinaciones de las características anteriormente mencionadas.
Así la etiqueta será si el precio de la media móvil aumentará mañana respecto a hoy.
x_1 = np.reshape(features_1_2[4:-1], (-1, 1))
x_2 = np.reshape(growth[:-1], (-1, 1))
x_3 = np.reshape(sentiment_score(tesla_stock, sentiments, 1)[4:-1], (-1, 1))
x_4 = np.concatenate((x_1, x_3), axis=1)
x_5 = np.concatenate((x_1, x_2, x_3), axis=1)
y = growth[1:]
from sklearn.model_selection import train_test_split as tts
from sklearn.tree import DecisionTreeClassifier
from sklearn.naive_bayes import BernoulliNB
from sklearn.metrics import classification_report
from sklearn.metrics import accuracy_score
Al predecir acerca de fluctuaciones en las acciones es común, no hacer un shuffle sobre los datos, es decir se entrena el modelo con datos de una etapa anterior para así ver si el aprendizaje obtenido aplica al futuro.
data = [x_1, x_2, x_3, x_4, x_5]
for i in range(len(data)):
data.append(tts(data.pop(0), y, test_size=0.3, shuffle=False))
Se usarán dos clasificadores, Árboles de Decisión y Naive Bayes con kernel Bernoulli, para así comparar el rendimiento de los modelos usando los distintos conjuntos de features definidos.
mod_dt = DecisionTreeClassifier()
mod_nb = BernoulliNB()
def fitPredRep(x_train, y_train, x_test, y_test, clf):
clf.fit(x_train, y_train)
y_pred = clf.predict(x_test)
print("Accuracy:", accuracy_score(y_pred, y_test))
print(classification_report(y_pred, y_test))
return y_pred
models = ["Num. de tweets ayer", "Crecimiento de media móvil hoy vs ayer", "Sentiment score de tweets de ayer",
"Num. de tweets ayer y Sentiment Score", "Todas las anteriores"]
dt_pred = []
nb_pred = []
print("Decision Tree Classifier")
for i in range(len(models)):
print("Features usados:", models[i])
x_tr, x_te, y_tr, y_te = data[i]
dt_pred.append(fitPredRep(x_tr, y_tr, x_te, y_te, mod_dt))
print("\n\n\n")
print("Bernoulli Naive Bayes")
for i in range(len(models)):
print("Features usados:", models[i])
x_tr, x_te, y_tr, y_te = data[i]
nb_pred.append(fitPredRep(x_tr, y_tr, x_te, y_te, mod_dt))
Se encuentra que el modelo de Árboles de Decisión con el feature del crecimiento de la media móvil hoy vs ayer, es el que mejor puede predecir si la media móvil aumentará mañana vs hoy, aún cuando se agregan más features.
A continuación, se muestra el Árbol de decisión que se usa para decidir el alza de la media móvil del precio de la acción:
Image(filename='graph.png')
from sklearn.metrics import confusion_matrix
def plot_confusion_matrix(y_true, y_pred,
normalize=False,
title=None,
cmap=plt.cm.Blues):
"""
This function prints and plots the confusion matrix.
Normalization can be applied by setting `normalize=True`.
"""
if not title:
if normalize:
title = 'Normalized confusion matrix'
else:
title = 'Confusion matrix, without normalization'
# Compute confusion matrix
cm = np.array([[186, 70], [54, 237]])
# Only use the labels that appear in the data
classes = ["Class 0", "Class 1"]
if normalize:
cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
print("Normalized confusion matrix")
else:
print('Confusion matrix, without normalization')
print(cm)
fig, ax = plt.subplots()
im = ax.imshow(cm, interpolation='nearest', cmap=cmap)
ax.figure.colorbar(im, ax=ax)
# We want to show all ticks...
ax.set(xticks=np.arange(cm.shape[1]),
yticks=np.arange(cm.shape[0]),
# ... and label them with the respective list entries
xticklabels=classes, yticklabels=classes,
title=title,
ylabel='True label',
xlabel='Predicted label')
# Rotate the tick labels and set their alignment.
plt.setp(ax.get_xticklabels(), rotation=45, ha="right",
rotation_mode="anchor")
# Loop over data dimensions and create text annotations.
fmt = '.2f' if normalize else 'd'
thresh = cm.max() / 2.
for i in range(cm.shape[0]):
for j in range(cm.shape[1]):
ax.text(j, i, format(cm[i, j], fmt),
ha="center", va="center",
color="white" if cm[i, j] > thresh else "black")
fig.tight_layout()
# plt.savefig("confusion.png")
return ax
Finalmente, se grafica la matriz de confusión del mejor modelo obtenido.
plot_confusion_matrix(data[1][-1], dt_pred[1], normalize=True, title='Matriz de confusión normalizada');
A continuación, se desea entrenar un modelo de regresión sobre el precio de la acción de Tesla para si es posible predecir el comportamiento bursátil de la empresa. Dado que hasta el momento se ha considerado como dato sólo el precio de Tesla, se considera necesario agregar otros inputs que otorguen información sobre el comportamiento de mercado en general, de esta manera se puede diferenciar entre lo que fue un cambio en bloque del mercado completo a diferencia de lo que puede ser un comportamiento aislado en el precio de la acción de Tesla.
Dado lo anterior, se consideran dos datasets extras:
A continuación, se cargan los datasets especificados y se muestra algunos datos de ejemplo.
nasdaq = pd.read_csv("^IXIC.csv")
ford = pd.read_csv("F.csv")
nasdaq_open = nasdaq["Open"].values
ford_open = ford["Open"].values
print('Muestra del dataset de Nasdaq')
display(nasdaq.head())
print('Muestra del dataset de precio de la acción de Ford')
display(ford.head())
tesla_open = tesla_stock["Open"].values
Se observa que dado que los datasets cuentan con distintas caracterizaciones para el precio en un día, se trabajará con el precio open, esto porque, como se mostró en un inicio, las distintas especificiaciones del precio siguen las mismas tendencias al graficarlas por lo que elegir una por sobre otra no debiera influir en el resultado del experimento.
En el siguiente par de líneas se chequea que los tamaños y las fechas de los precios registrados de los datasets sean iguales.
print(np.array_equal(ford["Date"].values, tesla_stock["Date"].values))
print(np.array_equal(nasdaq["Date"].values, tesla_stock["Date"].values))
Luego se dividen los datos en train-test.
nasdaq_train, nasdaq_test, ford_train, ford_test = tts(nasdaq_open[:-1], ford_open[:-1], shuffle=False, test_size=0.3)
tesla_train, tesla_test, tesla_open_train, tesla_open_test = tts(tesla_open[:-1], tesla_open[1:], shuffle=False, test_size=0.3)
Después, se normalizan los datos respecto al train set y se crean los features consistentes en los precios de 5 días anteriores, de cada índice económico.
nasdaq_mean, nasdaq_std = np.mean(nasdaq_train), np.std(nasdaq_train)
nasdaq_train_n = (nasdaq_train - nasdaq_mean) / nasdaq_std
nasdaq_test_n = (nasdaq_test - nasdaq_mean) / nasdaq_std
ford_mean, ford_std = np.mean(ford_train), np.std(ford_train)
ford_train_n = (ford_train - ford_mean) / ford_std
ford_test_n = (ford_test - ford_mean) / ford_std
tesla_mean, tesla_std = np.mean(tesla_train), np.std(tesla_train)
tesla_train_n = (tesla_train - tesla_mean) / tesla_std
tesla_test_n = (tesla_test - tesla_mean) / tesla_std
tesla_open_mean, tesla_open_std = np.mean(tesla_open_train), np.std(tesla_open_train)
tesla_open_train_n = (tesla_open_train - tesla_open_mean) / tesla_open_std
tesla_open_test_n = (tesla_open_test - tesla_open_mean) / tesla_open_std
num = nasdaq_train_n.shape[0] - 4
nasdaq_features_tr = np.zeros((num, 5))
ford_features_tr = np.zeros((num, 5))
tesla_features_tr = np.zeros((num, 5))
tesla_open_tr = np.expand_dims(tesla_open_train_n[4:], axis=-1)
for i in range(num):
nasdaq_features_tr[i, :] = nasdaq_train_n[i:i+5]
ford_features_tr[i, :] = ford_train_n[i:i+5]
tesla_features_tr[i, :] = tesla_train_n[i:i+5]
num = nasdaq_test_n.shape[0] - 4
nasdaq_features_te = np.zeros((num, 5))
ford_features_te = np.zeros((num, 5))
tesla_features_te = np.zeros((num, 5))
tesla_open_te = np.expand_dims(tesla_open_test_n[4:], axis=-1)
for i in range(num):
nasdaq_features_te[i, :] = nasdaq_test_n[i:i+5]
ford_features_te[i, :] = ford_test_n[i:i+5]
tesla_features_te[i, :] = tesla_test_n[i:i+5]
train_features_ = np.concatenate((np.ones((tesla_features_tr.shape[0], 1)),
tesla_features_tr, nasdaq_features_tr, ford_features_tr), axis=-1)
test_features_ = np.concatenate((np.ones((tesla_features_te.shape[0], 1)),
tesla_features_te, nasdaq_features_te, ford_features_te), axis=-1)
Se define el optimizador de Modelos lineales que usa "Normal Equation" para encontrar el ajuste:
def fit(X_, Y, rho=0):
# X_: arreglo de características, dim: [m, n_x + 1]
# Y: arreglo de etiquetas, dim: [m, 1]
# rho: parametro de regularización
# Realiza una regresión lineal (lineal no implica orden 1, la regresión es lineal en términos de los parámetros),
# del orden especificado para un vector de características y vector de salida (con dimensión 1),
# utilizando un parámetro de regularización rho.
# Usamos normal equation para obtener los parámetros de la regresión
Theta = np.linalg.inv(X_.transpose() @ X_ + rho * np.eye(X_.shape[1])) @ X_.transpose() @ Y
return Theta
Se define la función de costo (Error Cuadrático Medio):
cost = lambda Theta, X_, Y_: np.mean(np.square(np.ravel(X_ @ Theta) - np.ravel(Y_)))
Luego se ajusta el modelo usando varios valores para rho (el parámetro de regularización L2), para encontrar el modelo con mejor rendimineto (menor costo) en el conjunto de test. Se estudiará el ajuste en dos modelos, uno que use todos los índices bursátiles (Tesla, Nasdaq y Ford), que será el modelo A y otro que use sólo los de Tesla, que será el modelo B.
for rho in [0, 2, 5, 10, 15, 20]:
Theta = fit(train_features_, tesla_open_tr, rho)
plt.scatter(rho, cost(Theta, train_features_, tesla_open_tr), c='r', edgecolor="k")
plt.scatter(rho, cost(Theta, test_features_, tesla_open_te), c='b', edgecolor="k")
Theta = fit(train_features_[:, 0:6], tesla_open_tr, rho)
plt.scatter(rho, cost(Theta, train_features_[:, 0:6], tesla_open_tr), marker="*", c='r', edgecolor="k", s=120)
plt.scatter(rho, cost(Theta, test_features_[:, 0:6], tesla_open_te), marker="*", c='b', edgecolor="k", s=120)
plt.ylim([0, 0.03])
plt.title("Error Cuadrático Medio para el Modelo A y B \n usando distintos valores para rho")
plt.ylabel("Error Cuadrático Medio")
plt.xlabel("rho")
plt.legend(["ECM train set, A", "ECM test set, A", "ECM train set, B", "ECM test set, B"])
Se obtiene el mejor rendimiento para rho = 0 (sin regularización), por lo que se usará el Modelo B (más simple) sin regularización.
train_features_ = train_features_[:, :6]
test_features_ = test_features_[:, :6]
Theta = fit(train_features_, tesla_open_tr, rho=0)
Y_pred_train = (train_features_ @ Theta) * tesla_open_std + tesla_open_mean
Y_pred_test = (test_features_ @ Theta) * tesla_open_std + tesla_open_mean
m = test_features_.shape[0]
X_gen = np.ones((m, 6))
X_gen[0, :] = test_features_[0, :]
s = np.sqrt(cost(Theta, train_features_, tesla_open_tr))
for i in range(1, m):
X_gen[i, 1:5] = X_gen[i-1, 2:]
X_gen[i, 5] = (X_gen[i-1:i] @ Theta)[0, 0]
Y_pred_gen = (X_gen @ Theta) * tesla_open_std + tesla_open_mean
Graficando el ajuste en el conjunto de train y test:
converted_dates = list(map(datetime.datetime.strptime, tesla_stock["Date"], tesla_stock["Date"].values.shape[0]*['%Y-%m-%d']))
x_axis = converted_dates
formatter = dates.DateFormatter('%Y-%m-%d')
plt.figure(figsize=(15, 10))
y_axis = tesla_stock["Open"]
plt.plot(x_axis, y_axis, '-', c='b', label="Señal original")
plt.plot(x_axis[4:4+train_features_.shape[0]], Y_pred_train, c='r', label="Predición en Conjunto de Train")
plt.plot(x_axis[8+train_features_.shape[0]:8+train_features_.shape[0]+test_features_.shape[0]], Y_pred_test, c='g',
label="Predición en Conjunto de Test")
plt.title("Precio de las acciones de Tesla Inc. (TSLA) en el tiempo \n junto con el ajuste de modelos lineales", fontsize=24)
plt.xlabel("Fecha", fontsize=16)
plt.ylabel("Precio por acción ($USD)", fontsize=16)
ax = plt.gcf().axes[0]
ax.xaxis.set_major_formatter(formatter)
plt.gcf().autofmt_xdate(rotation=25)
plt.legend(prop={"size": 16})
plt.savefig("train_pred.png", dpi=300)
Se observa un buen ajuste en los datos, tanto en el conjunto de train como el de test, dada la calidad del ajuste, se puede observar que bastan sólo los índices de Tesla (no la información obtenida de Tweets u otros índices bursátiles), para predecir el precio de la acción a corto plazo.
A partir del trabajo realizado tanto de la exploración de datos como de los resultados obtenidos por los experimentos se pueden obtener interpretaciones de los tweets de Elon Musk y las repercuciones que tiene sobre el precio de la acción de la compañia Tesla inc.
De la exploración de datos se puede concluir que Elon Musk en el transcurso de los años ha aumentado la cantidad de Tweets que publica, ya que en el año 2010 solo tweeteo una vez en contraste con los más de 2500 tweet publicados en el año 2018. Dentro de sus tweets también se puede rescatar que las palabras más utilizadas tienen alguna relación con la campañia manejada por Elon Musk, por ejemplo "Tesla", "car", "@Tesla".
Para comenzar a realizar experimentos se realizó una vectorización de los tweets, por lo que se puede dar cuenta de la importancia de la dimensionalidad a utilizar, ya que viendo la representación en 2 dimensiones no se logra apreciar una separación útil de los datos que sirva para realizar clustering, por lo tanto al observar las agrupaciones de datos obtenidos no tienen ninguna representatividad. Por otra parte al aumentar la dimensionalidad y utilizando técnicas de reducción se logran obtener cluster bien definidos. De lo que se puede concluir que trabajar con dimensionalidad alta se transforma en una herramienta muy útil para trabajar datos que son considerados cercanos entre sí en una menor dimensionalidad.
De los cluster mediante DBSCAN obtenidos tampoco se pudo obtener información contundente a partir de los grupos, a excepción de los grupos pequeños que se pueden considerar como ruido.
Para la clasificación se utilizaron distintas cantidades de features y las combinaciones de ellos, tanto para los métodos de árbol de decisión y Bernoulli Naive Bayes. De lo que se puede obtener como conclusión que no se logra establecer una relación directa entre los tweets y la fluctuación del precio de la acción de la compañia, ya que trabajar con los datos de la bolsa entrega mucho mejores resultados de clasificación por sí solos que al agregar los datos de los tweets. Se concluye finalmente que no la hipótesis no tiene validez empírica.
Por el lado de la regresión, se obtuvo que la mejor predicción para el precio futuro de las acciones, se puede obtener con un modelo de Regresión Lineal que usa sólo los datos del índice bursátil de Tesla, donde agregar información de los tweets u otros índices bursátiles no entregaría una mejor predicción. Por lo anterior, se puede concluir que los tweets de Elon Musk no influyen de manera significativa en el precio de la acción, y si alguna vez lo hicieron, fueron sólo situaciones aísladas y no un comportamiento común por parte de Elon (como afirmó la SEC).